// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/btcturk/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/btcturk/order_v1/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::btcturk::order_v1 {

namespace {

using namespace coin2::exchange::btcturk::order_v1::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::btcturk::symbology::BtcturkCurrency;
using coin2::exchange::btcturk::symbology::BtcturkProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;

inline int64_t GetProcOrderId(const JsonValue& data) {
  if (data.HasMember("orderClientId") && data["orderClientId"].IsString()) {
    if (!data["orderClientId"].IsNull()) {
      return stoll(data["orderClientId"].GetString());
    }
  }
  return 0;
}

inline coin::proto::OrderSide GetOrderSide(const std::string& type) {
  if (type == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (type == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline coin::proto::OrderType GetOrderType(const std::string& method) {
  if (method == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (method == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void SetOrderStatus(ProductOrderElement* order, const std::string& status) {
  if ((status == "Untouched") || (status == "Partial")) {
    order->set_is_live(true);
  } else if ("Canceled" == status) {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if ("Closed" == status) {
    order->set_is_live(false);
    order->set_fully_filled(true);
  }
}

bool IsBtcturkSpotErrMessage(const JsonValue& doc) {
  if (doc.HasMember("code")) {
    return (std::string(doc["code"].GetString()) != "0");
  }

  return false;
}

}  // namespace

void BtcturkParser::ParseRestAccountBalance() {
  /*
  {
    "data": [
      {
        "asset": "TRY",
        "assetname": "Türk Lirası",
        "balance": "103158.9412490031968651",
        "locked": "1023.5699999896000000",
        "free": "102135.3712490135968651"
      },
      {
        "asset": "BTC",
        "assetname": "Bitcoin",
        "balance": "29.6027353000000000",
        "locked": "0.0010000000000000",
        "free": "29.6017353000000000"
      }
      ...
    ],
    "success": true,
    "message": null,
    "code": 0
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance BtcturkParser::ParseBalance(const JsonValue& doc) {
  AccountBalance acc_balance;

  if (doc.IsObject() && IsBtcturkSpotErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] Error message\n" << JsonToString(doc);
    return acc_balance;
  }

  if (!doc.HasMember("data") || !doc["data"].IsArray()) {
    LOG(ERROR) << "[ParseBalance] Invalid data format\n" << JsonToString(doc);
    return acc_balance;
  }

  acc_balance.set_exchange("Btcturk");
  acc_balance.set_market_type("Spot");
  for (const auto& data : doc["data"].GetArray()) {
    if (!data.HasMember("asset") || !data.HasMember("balance") || !data.HasMember("locked") ||
        !data.HasMember("free")) {
      continue;
    }
    if (!symcache_->has_currency_native(data["asset"].GetString())) {
      continue;
    }
    auto* currency = symcache_->GetCurrencyByNative(data["asset"].GetString());
    auto* currency_balance = acc_balance.add_each_balance();
    currency_balance->set_currency(currency->currency());
    currency_balance->set_currency_native(currency->native_currency());
    auto total = stod(data["balance"].GetString());
    auto hold = stod(data["locked"].GetString());
    currency_balance->set_total(total);
    currency_balance->set_hold(hold);
    currency_balance->set_available(total - hold);
  }

  return acc_balance;
}

void BtcturkParser::ParseRestOpenOrderInfoList() {
  /*
  {
    "success": true,
    "message": null,
    "code": 0,
    "data": {
      "asks": [{
        "id": 16060235,
        "price": "66800.00",
        "amount": "0.09733687",
        "quantity": "0.09733687",
        "stopPrice": "0.00",
        "pairSymbol": "BTCTRY",
        "pairSymbolNormalized": "BTC_TRY",
        "type": "sell",
        "method": "limit",
        "orderClientId": "da593000-6eb3-4a1c-ba26-c616122a0210",
        "time": 0,
        "updateTime": 1591286401373,
        "status": "Untouched",
        "leftAmount": "0.09733687"
      }],
      "bids": [{
        "id": 16071095,
        "price": "65817.00",
        "amount": "0.08956055",
        "quantity": "0.08956055",
        "stopPrice": "0.00",
        "pairSymbol": "BTCTRY",
        "pairSymbolNormalized": "BTC_TRY",
        "type": "buy",
        "method": "limit",
        "orderClientId": "da593000-6eb3-4a1c-ba26-c616122a0210",
        "time": 0,
        "updateTime": 1591352311273,
        "status": "Untouched",
        "leftAmount": "0.08956055"
      }]
    }
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsBtcturkSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].IsObject()) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  if (document["data"].HasMember("asks") && document["data"]["asks"].IsArray()) {
    for (const auto& ask_data : document["data"]["asks"].GetArray()) {
      auto order = product_order_bundle->add_each_order();
      ParseRestOpenOrder(order, ask_data);
      product_order_bundle_found = true;
    }
  }
  if (document["data"].HasMember("bids") && document["data"]["bids"].IsArray()) {
    for (const auto& bid_data : document["data"]["bids"].GetArray()) {
      auto order = product_order_bundle->add_each_order();
      ParseRestOpenOrder(order, bid_data);
      product_order_bundle_found = true;
    }
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void BtcturkParser::ParseRestOpenOrder(ProductOrderElement* order, const JsonValue& data) {
  order->set_exchange_type(ExchangeType::Btcturk);
  order->set_market_type(MarketType::Spot);
  order->set_api_version("v1");
  order->set_exchange_order_id(data["id"].GetString());
  order->set_symbol(data["pairSymbol"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(stod(data["amount"].GetString()));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data["type"].GetString()));
  order->set_order_type(GetOrderType(data["method"].GetString()));
  order->set_timestamp(GetCurrentTimestamp());
  SetOrderStatus(order, data["status"].GetString());
}

void BtcturkParser::ParseRestHistoryOrderInfoList() {
  /*
  {
    "success": true,
    "message": null,
    "code": 0,
    "data": [
      {
        "id": 9932534,
        "price": "20000.00",
        "amount": "0.001",
        "quantity": "0.001",
        "pairsymbol": "BTCTRY",
        "pairsymbolnormalized":"BTC_TRY",
        "type": "Buy",
        "method": "Limit",
        "orderClientId": "test",
        "time": 1543996112263,
        "updateTime": 1543996112263,
        "status": "Untouched"
      }
    ]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestHistoryOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsBtcturkSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestHistoryOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].IsArray()) {
    LOG(ERROR) << "[ParseRestHistoryOrderInfoList] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : document["data"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    ParseRestHistoryOrder(order, data);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void BtcturkParser::ParseRestHistoryOrder(ProductOrderElement* order, const JsonValue& data) {
  order->set_exchange_type(ExchangeType::Btcturk);
  order->set_market_type(MarketType::Spot);
  order->set_api_version("v1");
  order->set_exchange_order_id(data["id"].GetString());
  order->set_symbol(data["pairSymbol"].GetString());
  order->set_price(stod(data["price"].GetString()));
  order->set_qty(stod(data["amount"].GetString()));
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_side(GetOrderSide(data["type"].GetString()));
  order->set_order_type(GetOrderType(data["method"].GetString()));
  order->set_timestamp(GetCurrentTimestamp());
  SetOrderStatus(order, data["status"].GetString());
}

void BtcturkParser::ParseRestFillOrderInfoList() {
  /*
  {
    "success": true,
    "message": "SUCCESS",
    "code": 0,
    "data": [
      {
        "price": "313000.00",
        "numeratorSymbol": "BTC",
        "denominatorSymbol": "TRY",
        "orderType": "sell",
        "orderId": 22632304,
        "id": 5758565,
        "timestamp": 1628690323897,
        "amount": "-0.00040000",
        "fee": "-0.19098258",
        "tax": "-0.03437687"
      }
    ]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsBtcturkSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("data") || !document["data"].IsArray()) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : document["data"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    std::string numerator_symbol = data["numeratorSymbol"].GetString();
    std::string denominator_symbol = data["denominatorSymbol"].GetString();
    fill->set_symbol(numerator_symbol + denominator_symbol);
    fill->set_exchange_order_id(data["orderId"].GetString());
    fill->set_price(stod(data["price"].GetString()));
    fill->set_qty(std::abs(stod(data["amount"].GetString())));
    fill->set_fill_id(data["id"].GetString());
    if (data.HasMember("fee") && !data["fee"].IsNull()) {
      fill->set_fee(stod(data["fee"].GetString()));
    }
    if (data.HasMember("feeCurrency") && !data["feeCurrency"].IsNull()) {
      fill->set_fee_currency(data["feeCurrency"].GetString());
    }
    fill->set_fill_timestamp(GetCurrentTimestamp());
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void BtcturkParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("success") && json.HasMember("code") && json.HasMember("message")) {
    auto error_code = GetOrderErrorCode(json["success"], json["code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseSubmitResponse] Error response\n" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  resp->set_success(true);
  if (json.HasMember("data") && json["data"].HasMember("id")) {
    resp->set_exchange_order_id(std::string(json["data"]["id"].GetString()));
  } else {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(json);
  }
}

void BtcturkParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("success") && json.HasMember("code") && json.HasMember("message")) {
    auto error_code = GetOrderErrorCode(json["success"], json["code"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseCancelResponse] Error response\n" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

int32_t BtcturkParser::ParseErrorMessage(const std::shared_ptr<AsioHttpContext>& http_context) {
  enable_time_.success = false;

  const auto http_code = http_context->res.result_int();
  if (http_code == 400) {
    LOG(ERROR) << "[ParseErrorMessage] Bad Request\n" << msg_->DebugString();
  } else if (http_code == 401) {
    LOG(ERROR) << "[ParseErrorMessage] Unauthorized\n" << msg_->DebugString();
  } else if (http_code == 405) {
    LOG(ERROR) << "[ParseErrorMessage] Wrong Method\n" << msg_->DebugString();
  } else if (http_code == 429) {
    const auto& header = http_context->res.base();
    if (header.count("retry-after") > 0) {
      int32_t retry_after = std::stoi(header.at("retry-after").data());
      if (retry_after > 0) {
        enable_time_.enable_time = (retry_after + 2) * 1'000'000'000LL + GetCurrentTimestamp();
        enable_time_.success = true;
        LOG(INFO) << "btcturk native client enable_time: " << enable_time_.enable_time;
      }
    }
    LOG(ERROR) << "[ParseErrorMessage] Too Many Requests\n" << msg_->DebugString();
  } else if (http_code == 503) {
    LOG(ERROR) << "[ParseErrorMessage] Service Unavailable\n" << msg_->DebugString();
  } else if ((http_code >= 500) && (http_code < 600)) {
    LOG(ERROR) << "[ParseErrorMessage] Internal Server Error\n" << msg_->DebugString();
  }

  return http_code;
}

void BtcturkParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  NOTREACHED() << "[ParseWsMessage] WebSocket not supported";
}

auto BtcturkParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);

  int32_t error_code = ParseErrorMessage(msg.http_context);
  if (error_code == 429) {
    if (enable_time_.success) {
      mb_->PublishEvent("btcturk/enable_time", reinterpret_cast<void*>(enable_time_.enable_time));
    }
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrderInfoList();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestHistoryOrderInfoList();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillOrderInfoList();
      break;
    default: {
      LOG(ERROR) << "[ParseRawMessage] Unknown query type\n" << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::btcturk::order_v1
